Thomas Loiseau (TLoiseau-21)
Pour la réalisation de ce projet, nous allons nous étudier le classement du Vendée Globe 2020-2021. Pour cette édition, 33 participants se sont présenté sur la ligne de départ. Pour rappel, le Vendée Globe est à ce jour la plus grande course à la voile autour du monde, en solitaire, sans escale et sans assistance. La course longue de plus de plus de 44 900 kilomètres soit 24 296 milles à pour départ et arrivé la ville des Sables d'Olonne. Dans la réalité lors des huit précédentes éditions du Vendée Globe, la plupart des concurrents ont parcouru parfois plus de 28 000 milles (soit quasiment 52 000 kilomètres).
En effet, cette course est avant tout un voyage climatique pour descendre l'Atlantique, traverser l'océan Indien et le Pacifique, puis remonter de nouveau l'Atlantique... Les solitaires du Vendée Globe doivent en permanence jouer avec les systèmes météo. Ils sont composés d'anticyclones, zones de hautes pression plutôt stables et peu ventées et de dépressions, le plus souvent génératrices de vents forts. Le jeu consiste à trouver le bon équilibre : suffisamment loin des centres dépressionnaires pour éviter les vents les plus forts sans se faire engluer dans les hautes pressions. Il ne faut pas prendre non plus à la légére les courrant marains ainsi que les vagues de côtés qui peuvent faire dévier de cap voir faire chavirer le bateau.
Comme nous le verons par la suite, les données du dernier Vendée Globe sont disponibles sous la forme de fichiers Excel avec les classements fournis plusieurs fois par jour par les organisateurs de la course. Il y a également une page web avec une fiche technique par voilier qui contient des informations techniques et qu'il est possible de rapprocher des classements.
Ainsi dans ce rapport, nous allons dans un premier temps récupérer la donnée (téléchargement des fichiers excels puis les compiler ou bien les scrapper directement sur le site de la compétition). Dans un second temps nous allons nettoyer l'information afin de la rendre utilisable pour de prochaine analyse. Par la suite, nous allons nous pencher plus sur un aspect d'apprentissage statistique. En effet, nous allons essayer de prédire la vitesse et la distance parcoure par les marins en 30 minutes. Nous en profiterons pour faire quelques études préalables avec notamment quelques graphiques. Enfin, avant de conclure nous essayerons d'enrichir les données avec la météo (vent, courant marins, vagues) et relancer la même analyse afin de vérifier s'il y a bien une amélioration des performances de nos prédictions.
import pandas as pd
import numpy as np
from datetime import datetime
#pour le scraping
import requests
from bs4 import BeautifulSoup
import re
#pour l'ouverture de fichier excel
import os
import pylightxl as xl
#pour les cartes
import plotly.express as px
#pour le scraping de la météo
from time import sleep
from selenium import webdriver
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
#on deactive les warning, lié aux certifications lors du telechagement des excels ou lors de leur
#ouvertures (problème d'image)
import warnings
warnings.filterwarnings('ignore')
Tel que mentionné en introduction, les données du dernier Vendée Globe sont disponibles sous la forme de fichiers Excel avec les classements fournis plusieurs fois par jour par les organisateurs de la course. Ces fichiers contiennent des informations très importantes pour la suite de notre analyse, telles que l'horodatage (date et heure), la position géographique, le cap suivi, les vitesses et distances parcourues (en 30 minutes, 3 heures ou 24 heures) ainsi que la distance minimale restant à parcourir ou celle au premier du classement.
Dans un second temps, nous avons également une page web avec les fiches techniques de chaque voilier de la compétition. Nous pouvons ici y retrouver des caractéristiques à propos du bateau comme ses dimensions (longueur, Largeur, hauteur du mât), sur son poids (Tirant d'eau, Déplacement), la surface de ses voiles (voiles au près, voiles au portant) ou encore des données structurelles comme les matériaux utilisés pour la quille, son nombre de dérive, etc.
Dans cette partie nous allons télécharger la donnée sans toutefois la nettroyer (partie 2).
Comme nous le verrons ci-dessous, la récupération des fichiers excel ne peut pas se faire directement par pandas. En effet, comme mentionné dans la section Questions/Réponses de l'enoncé du projet, il y a parfois un bug avec pandas qui s'appuie à présent sur plusieur autres librairies pour récuperer les données. Malheursement, il ne sais pa bien gere la présence des photos/images. En ressort une erreur concernant l'argument 'xxid'.
pd.read_excel("https://www.vendeeglobe.org/download-race-data/vendeeglobe_20210303_080000.xlsx")
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) <ipython-input-3-beed4e9d709d> in <module> ----> 1 pd.read_excel("https://www.vendeeglobe.org/download-race-data/vendeeglobe_20210303_080000.xlsx") ~/opt/anaconda3/lib/python3.8/site-packages/pandas/util/_decorators.py in wrapper(*args, **kwargs) 209 else: 210 kwargs[new_arg_name] = new_arg_value --> 211 return func(*args, **kwargs) 212 213 return cast(F, wrapper) ~/opt/anaconda3/lib/python3.8/site-packages/pandas/util/_decorators.py in wrapper(*args, **kwargs) 329 stacklevel=find_stack_level(), 330 ) --> 331 return func(*args, **kwargs) 332 333 # error: "Callable[[VarArg(Any), KwArg(Any)], Any]" has no ~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/excel/_base.py in read_excel(io, sheet_name, header, names, index_col, usecols, squeeze, dtype, engine, converters, true_values, false_values, skiprows, nrows, na_values, keep_default_na, na_filter, verbose, parse_dates, date_parser, thousands, decimal, comment, skipfooter, convert_float, mangle_dupe_cols, storage_options) 480 if not isinstance(io, ExcelFile): 481 should_close = True --> 482 io = ExcelFile(io, storage_options=storage_options, engine=engine) 483 elif engine and engine != io.engine: 484 raise ValueError( ~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/excel/_base.py in __init__(self, path_or_buffer, engine, storage_options) 1693 self.storage_options = storage_options 1694 -> 1695 self._reader = self._engines[engine](self._io, storage_options=storage_options) 1696 1697 def __fspath__(self): ~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/excel/_openpyxl.py in __init__(self, filepath_or_buffer, storage_options) 555 """ 556 import_optional_dependency("openpyxl") --> 557 super().__init__(filepath_or_buffer, storage_options=storage_options) 558 559 @property ~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/excel/_base.py in __init__(self, filepath_or_buffer, storage_options) 543 self.handles.handle.seek(0) 544 try: --> 545 self.book = self.load_workbook(self.handles.handle) 546 except Exception: 547 self.close() ~/opt/anaconda3/lib/python3.8/site-packages/pandas/io/excel/_openpyxl.py in load_workbook(self, filepath_or_buffer) 566 from openpyxl import load_workbook 567 --> 568 return load_workbook( 569 filepath_or_buffer, read_only=True, data_only=True, keep_links=False 570 ) ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/reader/excel.py in load_workbook(filename, read_only, keep_vba, data_only, keep_links) 315 reader = ExcelReader(filename, read_only, keep_vba, 316 data_only, keep_links) --> 317 reader.read() 318 return reader.wb ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/reader/excel.py in read(self) 279 self.read_properties() 280 self.read_theme() --> 281 apply_stylesheet(self.archive, self.wb) 282 self.read_worksheets() 283 self.parser.assign_names() ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/styles/stylesheet.py in apply_stylesheet(archive, wb) 196 197 node = fromstring(src) --> 198 stylesheet = Stylesheet.from_tree(node) 199 200 wb._borders = IndexedList(stylesheet.borders) ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/styles/stylesheet.py in from_tree(cls, node) 101 for k in attrs: 102 del node.attrib[k] --> 103 return super(Stylesheet, cls).from_tree(node) 104 105 ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/descriptors/serialisable.py in from_tree(cls, node) 85 if hasattr(desc.expected_type, "from_tree"): 86 #complex type ---> 87 obj = desc.expected_type.from_tree(el) 88 else: 89 #primitive ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/descriptors/serialisable.py in from_tree(cls, node) 85 if hasattr(desc.expected_type, "from_tree"): 86 #complex type ---> 87 obj = desc.expected_type.from_tree(el) 88 else: 89 #primitive ~/opt/anaconda3/lib/python3.8/site-packages/openpyxl/descriptors/serialisable.py in from_tree(cls, node) 101 attrib[tag] = obj 102 --> 103 return cls(**attrib) 104 105 TypeError: __init__() got an unexpected keyword argument 'xxid'
Pour palier ce problème, il était conseillé de passer par la librairie xlwings. Malheureusement, pour une raison qui m'échappe encore, je ne suis pas parvenu à l'utiliser malgré le fragment de code donné. J'ai passé beaucoup de temps à trouver un moyen de lire les fichiers souhaités (facilement 3 jours entier de travail) avant de trouver qu'il était possible d'utiliser pylightxl la librairie dont se servait pandas.
La documentation de la librairie utilisé est diponible ici: https://pylightxl.readthedocs.io/_/downloads/en/latest/pdf/ https://pylightxl.readthedocs.io/en/latest/index.html
Ainsi le chargement des données de classement se fait en deux phases :
telechargement des données : get_all_race_classement_files qui scrap la liste des fichiers sur la page des classements et qui telecharge dans le dossier data
lecture et chargement des données dans un dataframe : classement_files_to_dataframe
def get_all_race_classement_files(base_url, page, directory_name):
req = requests.get(base_url+page)
soup = BeautifulSoup(req.content)
options = soup.find_all("option", value = re.compile("\d{8}_\d{6}"))
download_page = soup.find("a", "rankings__download")["href"]
for i, o in enumerate(options):
file_name = o["value"]
download_page = re.sub("\d{8}_\d{6}", file_name, download_page)
file_name = download_page.split("/")[-1]
req_option = requests.get(base_url+download_page, verify=False)
with open("{}/{}".format(directory_name,file_name),'wb') as output_file:
output_file.write(req_option.content)
get_all_race_classement_files("https://www.vendeeglobe.org/","fr/classement/20210303_080000", "data")
def classement_files_to_dataframe(directory_name):
df_result = pd.DataFrame()
for file in os.listdir(directory_name):
if file.endswith(".xlsx"):
#on lit notre fichier excel onglet fr
db = xl.readxl(fn="{}/{}".format(directory_name,file), ws=('fr'))
# On recuprer les colonnes du fichier qu'on lie qu'a partir de la 6 eme ligne
col_list = []
for col in db.ws(ws='fr').cols:
col_list.append(col[5:])
columns_names =["","Rank", "Nat. / Sail","Skipper / crew","Hour FR","Latitude","Longitude",
"30 minutes Heading","30 minutes Speed","30 minutes VMG","30 minutes Distance",
"last report Heading","last report Speed","last report VMG","last report Distance",
"24 hours Heading","24 hours Speed","24 hours VMG","24 hours Distance",
"DTF","DTL"
]
#on extrait la date du nom du fichier
date = re.findall("_(\d{4})([0-9]{2})(\d{2})_", file)
year = [date[0][0]]*len(col_list[0]) # cree un vecteur du nombre de ligne du fichier excel
month = [date[0][1]]*len(col_list[0])
day = [date[0][2]]*len(col_list[0])
dict_for_df = {"Year":year, "Month": month, "Day": day}
for i, col_name in enumerate(columns_names):
dict_for_df[col_name] = col_list[i]
df_excel = pd.DataFrame( dict_for_df )
df_excel.drop("", axis=1)
df_result = pd.concat([df_result, df_excel], ignore_index=True)
return df_result
df_classement = classement_files_to_dataframe("data")
df_classement
| Year | Month | Day | Rank | Nat. / Sail | Skipper / crew | Hour FR | Latitude | Longitude | ... | last report Heading | last report Speed | last report VMG | last report Distance | 24 hours Heading | 24 hours Speed | 24 hours VMG | 24 hours Distance | DTF | DTL | ||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 2021 | 01 | 29 | 1\nARV | \nFRA 17 | Yannick Bestaven\nMaître Coq IV | ... | 12.6 kts | 24365.7 nm | 117.3 % | 14.8 kts | 28583.8 nm | |||||||||
| 1 | 2021 | 01 | 29 | 2\nARV | \nFRA 79 | Charlie Dalin\nAPIVIA | ... | 02h 31min 01s | 02h 31min 01s | 12.6 kts | 24365.7 nm | 119.6 % | 15.1 kts | 29135.0 nm | |||||||
| 2 | 2021 | 01 | 29 | 3\nARV | \nFRA 18 | Louis Burton\nBureau Vallée 2 | ... | 06h 40min 26s | 04h 09min 25s | 12.6 kts | 24365.7 nm | 117.6 % | 14.8 kts | 28650.0 nm | |||||||
| 3 | 2021 | 01 | 29 | 4\nARV | \nFRA 01 | Jean Le Cam\nYes we Cam ! | ... | 10h 00min 09s | 03h 19min 43s | 12.5 kts | 24365.7 nm | 112.9 % | 14.1 kts | 27501.5 nm | |||||||
| 4 | 2021 | 01 | 29 | 5\nARV | \nMON 10 | Boris Herrmann\nSeaexplorer - Yacht Club De Mo... | ... | 11h 14min 59s | 01h 14min 50s | 12.6 kts | 24365.7 nm | 116.8 % | 14.7 kts | 28448.5 nm | |||||||
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 26444 | 2021 | 02 | 19 | RET | \nFRA 6 | Nicolas Troussel\nCORUM L'Épargne | ... | ||||||||||||||
| 26445 | 2021 | 02 | 19 | Traitements et calculs : Géovoile, un service ... | ... | ||||||||||||||||
| 26446 | 2021 | 02 | 19 | ... | |||||||||||||||||
| 26447 | 2021 | 02 | 19 | VMG : Velocity Made Good = projection du vecte... | ... | ||||||||||||||||
| 26448 | 2021 | 02 | 19 | DTF : Distance To Finish = Distance théorique ... | ... |
26449 rows × 24 columns
Cette partie m'a posé beaucoup moins de difficultés que la précédente. Ainsi à partir de BeautifulSoup il est très aisé de récupré dans le code source de la parge les informations nécessaires et de les structurer dans un dataframe.
def get_all_boats_data(base_url, page):
df_boats_data = pd.DataFrame()
req = requests.get(base_url+page)
soup = BeautifulSoup(req.content)
boats_list = soup.find_all("div", "boats-list__popup-infos")
for i, boat in enumerate(boats_list):
boat_data={}
boat_data["Name"] = [boat.find("h3", "boats-list__popup-title").text.strip()]
caracteristiques = boat.find_all("li")
for j, caracteristique in enumerate(caracteristiques):
caracteristique_split = caracteristique.text.split(":")
boat_data[caracteristique_split[0].strip()] = [caracteristique_split[1].strip()]
new_boat_data = pd.DataFrame(boat_data)
df_boats_data = pd.concat([df_boats_data,new_boat_data], ignore_index=True)
return df_boats_data
df_all_boats_data = get_all_boats_data("https://www.vendeeglobe.org/","/fr/glossaire")
df_all_boats_data
| Name | Numéro de voile | Anciens noms du bateau | Architecte | Chantier | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Nombre de dérives | Hauteur mât | Voile quille | Surface de voiles au près | Surface de voiles au portant | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | NEWREST - ART & FENÊTRES | FRA 56 | No Way Back, Vento di Sardegna | VPLP/Verdier | Persico Marine | 01 Août 2015 | 18,28 m | 5,85 m | 4,50 m | 7 t | foils | 29 m | monotype | 320 m2 | 570 m2 |
| 1 | PURE - Best Western® | FRA 49 | Gitana Eighty, Synerciel, Newrest-Matmut | Bruce Farr Design | Southern Ocean Marine (Nouvelle Zélande) | 08 Mars 2007 | 18,28m | 5,80m | 4,50m | 9t | 2 | 28m | acier forgé | 280 m2 | 560 m2 |
| 2 | TSE - 4MYPLANET | FRA72 | Famille Mary-Etamine du Lys, Initiatives Coeur... | Marc Lombard | MAG France | 01 Mars 1998 | 18,28m | 5,54m | 4,50m | 9t | 2 | 29 m | acier | 260 m2 | 580 m2 |
| 3 | Maître CoQ IV | 17 | Safran 2 - Des Voiles et Vous | Verdier - VPLP | CDK Technologies | 12 Mars 2015 | 18,28 m | 5,80 m | 4,50 m | 8 t | foils | 29 m | acier mécano soudé | 310 m2 | 550 m2 |
| 4 | CHARAL | 08 | NaN | VPLP | CDK Technologies | 18 Août 2018 | 18,28 m | 5,85 m | 4,50 m | 8t | foils | 29 m | acier | 320 m2 | 600 m2 |
| 5 | LA MIE CÂLINE - ARTISANS ARTIPÔLE | FRA 14 | Ecover3, Président, Gamesa, Kilcullen Voyager-... | Owen Clarke Design LLP - Clay Oliver | Hakes Marine - Mer Agitée | 03 Août 2007 | 18,28 m | 5,65 m | 4,50 m | 7,9 tonnes | foils | 29 m | basculante avec vérin | 300 m² | 610 m² |
| 6 | BUREAU VALLEE 2 | 18 | Banque Populaire VIII | Verdier - VPLP | CDK Technologies | 09 Juin 2015 | 18,28 m | 5,80 m | 4,50 m | 7,6 t | foils | 28 m | acier | 300 m2 | 600 m2 |
| 7 | ONE PLANET ONE OCEAN | ESP 33 | Kingfisher - Educacion sin Fronteras - Forum M... | Owen Clarke Design | Martens Yachts | 02 Février 2000 | 18,28 m | 5,30 m | 4,50 m | 8,9 t | 2 | 26 m | acier | 240 m2 | 470 m2 |
| 8 | GROUPE SÉTIN | FRA 71 | Paprec-Virbac2, Estrella Damm, We are Water, L... | Bruce Farr Yacht Design | Southern Ocean Marine (Nouvelle-Zélande) | 02 Février 2007 | 18,28 m | 5,80 m | 4,50 m | 9 t | 2 asymétriques | 28,50 | basculante sur vérin hydraulique | 270 m2 | 560 m2 |
| 9 | BANQUE POPULAIRE X | FRA30 | Macif - SMA | Verdier - VPLP | CDK - Mer Agitée | 01 Mars 2011 | 18,28 m | 5,70 m | 4,5 m | 7,7 t | 2 | 29 m | acier forgé | 340 m2 | 570 m2 |
| 10 | APIVIA | FRA 79 | NaN | Verdier | CDK technologies - MerConcept | 05 Août 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier | 350 m2 | 560 m2 |
| 11 | INITIATIVES-COEUR | FRA109 | Maître CoQ - Banque Populaire VII- Foncia II | VPLP - Verdier | CDK Technologies | 20 Septembre 2010 | 18,28 m | 5,70 m | 4,50 m | 7,8 t | foils | 27 m | acier forgé | 300 m2 | 600 m2 |
| 12 | MERCI | 69 | Foresight Natural Energy, Maisonneuve | Lavanos | Artech do Brasil | 15 Janvier 2005 | 18,28 m | 5,60 m | 4,50 m | 8,5 t | 2 | 29 m | acier | 250 m2 | 650 m2 |
| 13 | OMIA - WATER FAMILY | FRA09 | Spirit of Yukoh, Neutrogena, Hugo Boss, Estrel... | Bruce Farr Design | Offshore Challenge - Cowes | 03 Juillet 2007 | 18,28 m | 5,85 m | 4,50 m | 8 t | 2 | 29 m | acier forgé | 300 m2 | 700 m2 |
| 14 | PRB | FRA 85 | NaN | Verdier - VPLP | CDK Technologies | 08 Mars 2010 | 18,28 m | 5,50 m | 4,50 m | NC | foils | 27,40 m | Acier mécano soudé | 300 m2 | 600 m2 |
| 15 | Compagnie du Lit / Jiliti | FRA83 | Delta Dore, Bureau Vallée, Vers un Monde sans ... | Bruce Farr design | JMV Cherbourg | 26 Juillet 2006 | 18,28 m | 5,75 m | 4,50 m | 8,5 t | 2 | 29 m | acier forgé | 300 m2 | 620 m2 |
| 16 | NaN | NaN | NaN | NaN | 01 Janvier 1970 | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | |
| 17 | MEDALLIA | GBR77 | Armor Lux, We Are Water, La Fabrique | Pierre Rolland | Bernard Stamm | 03 Juillet 1999 | 18,28 m | 5,70 m | 4,50 m | 9 t | 2 | 29 m | carbone | 300 m2 | 580 m2 |
| 18 | SEAEXPLORER - YACHT CLUB DE MONACO | 16 | Edmond de Rothschild, Malizia - Yacht Club de ... | Verdier - VPLP | Multiplast | 07 Août 2015 | 18,28 m | 5,70 m | 4,50 m | 7,6 t | foils | 29 m | acier | 290 m2 | 490 m2 |
| 19 | STARK | FIN222 | Aviva, GAES Centros Auditivos | Owen Clarke Design | Hakes Marine - Wellington (Nouvelle-Zélande) | 06 Août 2007 | 18,28 m | 5,80 m | 4,50 m | 8,5 t | 2 | 28 m | acier | 270 m2 | 580 m2 |
| 20 | MACSF | FRA 27 | Quéguiner, Safran | Verdier - VPLP | Chantier Naval de Larros | 04 Juillet 2007 | 18,28 m | 5,60 m | 4,50 m | 7,7 t | foils | 27 m | acier | 300 m2 | 650 m2 |
| 21 | Yes We Cam! | 001 | Cheminées Poujoulat, Mare, Maître CoQ, Mapfre,... | Bruce Farr design | CDK Technologies | 03 Janvier 2007 | 18,28 m | 5,90 m | 4,50 m | 8 t | 2 | 28 m | acier forgé | 300 m2 | 620 m2 |
| 22 | TIME FOR OCEANS | FRA 92 | Hugo Boss, Energa, Compagnie du Lit-Boulogne B... | Finot-Conq Design | Neville Hutton | 01 Juin 2007 | 18,28 m | 5,84 m | 4,50 m | 8,5 t | foils | 28 m | monotype | 300 m2 | 550 m2 |
| 23 | CAMPAGNE DE FRANCE | FRA50 | Great America IV, Mirabaud, Temenos | Owen Clarke | Southern Ocean Marine, Tauranga | 04 Mai 2006 | 18,28 m | 5,50 m | 4,50 m | 8,5 t | 2 | 28 m | carbone | 330 m2 | 600 m2 |
| 24 | PRYSMIAN GROUP | ITA 34 | Saint-Michel - Virbac | VPLP - Verdier | Multiplast | 02 Avril 2015 | 18,28 m | 5,80 m | 4,5 m | 8 t | foils | 29 m | acier forgé | 300 m2 | 600 m2 |
| 25 | LA FABRIQUE | SUI07 | Brit Air, Votre Nom autour du Monde, MACSF | Groupe Finot-Conq | Multiplast | 01 Août 2007 | 18,28 m | 5,90 m | 4,50 m | nc | foils | 27 m | carbone | 290 m2 | 580 m2 |
| 26 | LinkedOut | NaN | NaN | Verdier | Persico | 03 Septembre 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier forgé | 350 m2 | 560 m2 |
| 27 | GROUPE APICIL | FRA1000 | Comme Un Seul Homme, DCNS | Groupe Finot-Conq | Multiplast | 10 Août 2008 | 18,28 m | 5,85 m | 4,50 m | 8,5 t | 2 | 29 m | acier forgé | 350 m2 | 610 m2 |
| 28 | DMG MORI Global One | JPN 11 | NaN | VPLP | Multiplast | 05 Septembre 2019 | 18,28 m | 5,85 m | 4,50 m | 8 t | foils | 29 m | acier forgé | 320 m2 | 580 m2 |
| 29 | ARKEA PAPREC | 4 | NaN | Juan Kouyoumdjian | CDK Technologies / Assemblage | 19 Juillet 2019 | 18,28 m | 5,70 m | 4,50 m | 8 t | foiler | 29 m | Inox usiné | 260 m2 | 600 m2 |
| 30 | V and B-MAYENNE | FRA53 | Le Souffle du Nord, Groupe Bel | Verdier - VPLP | Indiana Yachting (Scarlino, Italie) | 07 Septembre 2007 | 18,28 m | 5,50 m | 4,50 m | 7,7 t | 2 | 29 m | acier | 365 m2 | 700 m2 |
| 31 | HUGO BOSS | GBR 99 | NaN | VPLP - Alex Thomson Racing (led by Pete Hobson) | Carrington Boats | 15 Août 2019 | 18,28 m | 5,40 m | 4,50 m | 7,7 t | foils | 29 m | acier forgé | 330 m2 | 630 m2 |
| 32 | L'OCCITANE EN PROVENCE | 2 | NaN | Samuel Manuard | Black Pepper© | 31 Janvier 2020 | 18,28 m | 5,50 m | 4,50 m | 7,8 t | foils | 28 m | acier forgé | 270 m2 | 535 m2 |
| 33 | CORUM L'EPARGNE | FRA 6 | NaN | Juan Kouyoumdjian | CDK technologies - Mer Agitée | 15 Mai 2020 | 18,28 m | 5,70 m | 4,50 m | 7,9 t | foils | 27,30 m | NaN | 270 m2 | 535 m2 |
Comme nous pouvons le voir dans la partie précédante, nous devons impérativement nettoyer la donné afin de la rendre utilisable pour les modèles d’analyses que nous souhaitons mettre par la suite en place. En effet, la donnée brute telle que nous l'avons récupérée dans l'étape précédente, la donnée comporte de nombreuses case vide, des valeurs nomerique avec leurs unités accollées, de trop nobreuses valeurs catégorielles, etc.
Nous allons dans cette section exposer les corrections apportés au deux dataframes précédemment créés.
Le dataframe des classements réguliers de la compétition à nécessité dans un premier temps la supressions de nombreux uplets parasites. En effet, à partir du moment ou un participant depasse la ligne d'arrivé, le fichier excel est alors scind en deux parties : tout d'abors le classement à l'arrivé n'ayant les temps de courses totaux, vient ensuite le tableau "normal" de la progression du skipper entre chaque relevé. Ainsi pour nettoyer les participants déjà arrivés ou ayant abandonnés, non supprime dans un premier temps toutes les lignes ayant un attribut latitude ou un rang caractere vide (""), puis nous conservons uniquement les lignes que ne possedent pas de texte dans leurs rangs (ARV ou RET)
Par la suite, le nettoyage à principalement consité à extraire de partie numériques des cellules et de les convertir dans le bon format (int ou float)
def convert_degree_to_decimal(degree_string):
degree = re.findall("(\d+)°(\d+).(\d+)'([A-Z])",degree_string)
decimal = int(degree[0][0]) + int(degree[0][1])/60 + int(degree[0][2])/3600
if degree[0][-1] in ["S", "W"]:
decimal = -decimal
return "%.4f" % decimal
def clean_classement(df_classement):
#on supprime toute les valeurs nulles ""
df_classement = df_classement[df_classement.Latitude != ""]
df_classement = df_classement[df_classement.Rank != ""]
#On supprime tous les participants ayant abandonnés ou ayant fini
df_classement = df_classement[~ df_classement['Rank'].str.contains("[A-Za-z]")]
df_classement['Rank'] = df_classement['Rank'].astype(int)
#on recupere uniquement le numero du bateau
df_classement["Nat. / Sail"] = df_classement["Nat. / Sail"].str.findall("(\d+)").str[0].astype(int)
#On extrait le nom du marain
df_classement["Skipper / crew"] = df_classement["Skipper / crew"].str.split("\n").str[0].str.strip()
#On transforme la latritude et longitude en decimal
df_classement["Latitude"] = df_classement["Latitude"].apply(lambda x : convert_degree_to_decimal(x) ).astype(float)
df_classement["Longitude"] = df_classement["Longitude"].apply(lambda x : convert_degree_to_decimal(x) ).astype(float)
#on convertie le reste des colonnes en valeurs numériques
float_column = ["30 minutes Heading","30 minutes Speed","30 minutes VMG","30 minutes Distance",
"last report Heading","last report Speed","last report VMG","last report Distance",
"24 hours Heading","24 hours Speed","24 hours VMG","24 hours Distance",
"DTF","DTL"]
for i, col in enumerate(float_column):
df_classement[col] = df_classement[col].str.replace(',', '.').str.findall(r"[-+]?(?:\d*\.\d+|\d+)").str[0].astype(float)
#on clean les horodatage
df_classement["Hour FR"] = df_classement["Hour FR"].str.findall("(\d{2}:\d{2})").str[0]#.str.join('')
df_classement["Horodatage"] = (df_classement['Year']
.str.cat(df_classement['Month'], sep='/')
.str.cat(df_classement['Day'], sep='/')
.str.cat(df_classement['Hour FR'], sep=' ')
)
df_classement["Horodatage"] = pd.to_datetime(df_classement["Horodatage"])
#on supprime les colonnes dont on n'a plus besoin
df_classement = df_classement.drop(['Year', 'Month', 'Day','Hour FR', ""], axis=1)
return df_classement.sort_values(by=["Horodatage"], ignore_index=True)
df_classement_clean = clean_classement(df_classement)
df_classement_clean
| Rank | Nat. / Sail | Skipper / crew | Latitude | Longitude | 30 minutes Heading | 30 minutes Speed | 30 minutes VMG | 30 minutes Distance | last report Heading | last report Speed | last report VMG | last report Distance | 24 hours Heading | 24 hours Speed | 24 hours VMG | 24 hours Distance | DTF | DTL | Horodatage | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 28 | 27 | Isabelle Joschke | 46.4272 | -1.8061 | 238.0 | 13.8 | 13.5 | 0.2 | 358.0 | 0.0 | 0.0 | 2788.5 | 187.0 | 0.2 | 0.2 | 5.2 | 24295.5 | 1.6 | 2020-11-08 15:26:00 |
| 1 | 20 | 6 | Nicolas Troussel | 46.4389 | -1.8219 | 241.0 | 22.3 | 22.3 | 0.4 | 357.0 | 0.0 | 0.0 | 2789.3 | 195.0 | 0.2 | 0.1 | 4.5 | 24295.2 | 1.4 | 2020-11-08 15:27:00 |
| 2 | 14 | 1000 | Damien Seguin | 46.4233 | -1.8172 | 232.0 | 7.5 | 7.2 | 2.7 | 357.0 | 0.0 | 0.0 | 2788.4 | 192.0 | 0.2 | 0.2 | 5.4 | 24294.9 | 1.1 | 2020-11-08 15:28:00 |
| 3 | 26 | 2 | Armel Tripon | 46.4267 | -1.8175 | 242.0 | 12.6 | 12.6 | 0.6 | 358.0 | 0.0 | 0.0 | 2788.9 | 190.0 | 0.2 | 0.2 | 4.8 | 24295.4 | 1.5 | 2020-11-08 15:28:00 |
| 4 | 30 | 50 | Miranda Merron | 46.4275 | -1.8094 | 237.0 | 11.4 | 11.3 | 0.4 | 358.0 | 0.0 | 0.0 | 2788.9 | 188.0 | 0.2 | 0.2 | 4.8 | 24295.6 | 1.7 | 2020-11-08 15:28:00 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 15267 | 25 | 222 | Ari Huusela | 47.1531 | -5.5864 | 19.0 | 5.7 | 0.5 | 2.8 | 28.0 | 7.6 | 2.8 | 22.7 | 66.0 | 8.2 | 7.9 | 196.5 | 161.4 | 0.0 | 2021-03-04 11:30:00 |
| 15268 | 25 | 222 | Ari Huusela | 47.2228 | -5.3525 | 106.0 | 6.7 | 6.7 | 3.4 | 69.0 | 3.5 | 2.9 | 10.5 | 62.0 | 7.4 | 7.1 | 178.8 | 152.8 | 0.0 | 2021-03-04 14:30:00 |
| 15269 | 25 | 222 | Ari Huusela | 47.1925 | -4.7100 | 94.0 | 10.0 | 9.7 | 5.0 | 94.0 | 8.7 | 8.5 | 26.1 | 62.0 | 7.2 | 6.9 | 172.2 | 127.4 | 0.0 | 2021-03-04 17:30:00 |
| 15270 | 25 | 222 | Ari Huusela | 47.1456 | -3.8250 | 107.0 | 8.7 | 8.6 | 4.4 | 94.0 | 9.1 | 8.8 | 36.5 | 68.0 | 7.1 | 6.9 | 170.9 | 92.7 | 0.0 | 2021-03-04 21:30:00 |
| 15271 | 25 | 222 | Ari Huusela | 46.7225 | -2.4422 | 128.0 | 11.4 | 11.3 | 1.0 | 115.0 | 8.9 | 8.9 | 62.2 | 84.0 | 6.7 | 6.7 | 161.5 | 30.6 | 0.0 | 2021-03-05 04:30:00 |
15272 rows × 20 columns
Pas grand-chose à expliquer non plus pour cette partie, le dataframe a surtout subi de l'extraction et du formatage de valeurs numériques.
Notons toutefois que nous avons munuellement complété le numéro de participant de l'un des skippers ainsi que nettoyer les valeurs catégorielles des architectes de bateaux. Cette variable ainsi que "voile quille" et "Nombre de dérives" sont à la fin transformés en variables 1hot.
Enfin, nous avons fait le choix de supprimer les variables "Name","Anciens noms du bateau","Chantier" car celles-ci sont toutes différentes et n'apportent pas d'informations utiles.
def clean_all_boats_data(df_all_boats_data):
#On redonne le numero de participant manquant ou erroné
df_all_boats_data.loc[df_all_boats_data.Name =="LinkedOut","Numéro de voile"] = "FRA 59"
df_all_boats_data.loc[df_all_boats_data["Numéro de voile"] =="16","Numéro de voile"] = "MON 10"
df_all_boats_data.loc[df_all_boats_data["Numéro de voile"] =="GBR77","Numéro de voile"] = "GBR 777"
#on complete le type de Voile quille (avant NaN)
df_all_boats_data.loc[df_all_boats_data["Numéro de voile"] == "FRA 6","Voile quille"] = ""
#on supprime les colonnes qui ne nous interessent pas pour nos futures analyses
df_all_boats_data = df_all_boats_data.drop(["Name","Anciens noms du bateau","Chantier"], axis=1)
#On supprime le bateau sans information (celui de François Guiffant)
df_all_boats_data = df_all_boats_data.dropna()
#On ne conserve que la partie entiere du numero
df_all_boats_data["Numéro de voile"] = df_all_boats_data["Numéro de voile"].str.findall("(\d+)").str[0].astype(int)
#on extrait l’année de lancement du bateau
df_all_boats_data["Date de lancement"] = df_all_boats_data["Date de lancement"].str.findall("(\d{4})").str[0].astype(int)
#on nettoye les valeurs des architectes
map_architect = {
"Verdier - VPLP" : "Verdier - VPLP",
"Bruce Farr Design" : "Bruce Farr Design",
"Owen Clarke Design" : "Owen Clarke Design",
"VPLP - Verdier" : "Verdier - VPLP",
"Bruce Farr design" : "Bruce Farr Design",
"Groupe Finot-Conq" : "Groupe Finot-Conq",
"VPLP/Verdier" : "Verdier - VPLP",
"Marc Lombard" : "Marc Lombard",
"Owen Clarke Design LLP - Clay Oliver" : "Owen Clarke Design",
"Bruce Farr Yacht Design" : "Bruce Farr Design",
"Lavanos" : "Lavanos",
"Pierre Rolland" : "Pierre Rolland",
"Finot-Conq Design" : "Groupe Finot-Conq",
"Owen Clarke" : "Owen Clarke Design"
}
df_all_boats_data = df_all_boats_data.replace({"Architecte": map_architect})
#On Extrait la partie numerique des colonnes souhaités
float_column = ["Longueur", "Largeur", "Tirant d'eau", "Déplacement (poids)", "Hauteur mât",
"Surface de voiles au près", "Surface de voiles au portant"]
for i, col in enumerate(float_column):
df_all_boats_data[col] = df_all_boats_data[col].str.replace(',', '.').str.findall(r"[-+]?(?:\d*\.\d+|\d+)").str[0].astype(float)
# On retourne le dataframe avec les varibles catégorielle codé en 1hot
df_all_boats_data = pd.get_dummies(df_all_boats_data)
# Remplace les valeurs manquante par une moyenne
return df_all_boats_data.fillna(df_all_boats_data.mean())
df_all_boats_data_clean = clean_all_boats_data(df_all_boats_data)
df_all_boats_data_clean
| Numéro de voile | Date de lancement | Longueur | Largeur | Tirant d'eau | Déplacement (poids) | Hauteur mât | Surface de voiles au près | Surface de voiles au portant | Architecte_Bruce Farr Design | ... | Voile quille_ | Voile quille_Acier mécano soudé | Voile quille_Inox usiné | Voile quille_acier | Voile quille_acier forgé | Voile quille_acier mécano soudé | Voile quille_basculante avec vérin | Voile quille_basculante sur vérin hydraulique | Voile quille_carbone | Voile quille_monotype | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 56 | 2015 | 18.28 | 5.85 | 4.5 | 7.00000 | 29.0 | 320.0 | 570.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| 1 | 49 | 2007 | 18.28 | 5.80 | 4.5 | 9.00000 | 28.0 | 280.0 | 560.0 | 1 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 2 | 72 | 1998 | 18.28 | 5.54 | 4.5 | 9.00000 | 29.0 | 260.0 | 580.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 3 | 17 | 2015 | 18.28 | 5.80 | 4.5 | 8.00000 | 29.0 | 310.0 | 550.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 |
| 4 | 8 | 2018 | 18.28 | 5.85 | 4.5 | 8.00000 | 29.0 | 320.0 | 600.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 5 | 14 | 2007 | 18.28 | 5.65 | 4.5 | 7.90000 | 29.0 | 300.0 | 610.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| 6 | 18 | 2015 | 18.28 | 5.80 | 4.5 | 7.60000 | 28.0 | 300.0 | 600.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 7 | 33 | 2000 | 18.28 | 5.30 | 4.5 | 8.90000 | 26.0 | 240.0 | 470.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 8 | 71 | 2007 | 18.28 | 5.80 | 4.5 | 9.00000 | 28.5 | 270.0 | 560.0 | 1 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| 9 | 30 | 2011 | 18.28 | 5.70 | 4.5 | 7.70000 | 29.0 | 340.0 | 570.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 10 | 79 | 2019 | 18.28 | 5.85 | 4.5 | 8.00000 | 29.0 | 350.0 | 560.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 11 | 109 | 2010 | 18.28 | 5.70 | 4.5 | 7.80000 | 27.0 | 300.0 | 600.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 12 | 69 | 2005 | 18.28 | 5.60 | 4.5 | 8.50000 | 29.0 | 250.0 | 650.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 13 | 9 | 2007 | 18.28 | 5.85 | 4.5 | 8.00000 | 29.0 | 300.0 | 700.0 | 1 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 14 | 85 | 2010 | 18.28 | 5.50 | 4.5 | 8.13871 | 27.4 | 300.0 | 600.0 | 0 | ... | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 15 | 83 | 2006 | 18.28 | 5.75 | 4.5 | 8.50000 | 29.0 | 300.0 | 620.0 | 1 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 17 | 777 | 1999 | 18.28 | 5.70 | 4.5 | 9.00000 | 29.0 | 300.0 | 580.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| 18 | 10 | 2015 | 18.28 | 5.70 | 4.5 | 7.60000 | 29.0 | 290.0 | 490.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 19 | 222 | 2007 | 18.28 | 5.80 | 4.5 | 8.50000 | 28.0 | 270.0 | 580.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 20 | 27 | 2007 | 18.28 | 5.60 | 4.5 | 7.70000 | 27.0 | 300.0 | 650.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 21 | 1 | 2007 | 18.28 | 5.90 | 4.5 | 8.00000 | 28.0 | 300.0 | 620.0 | 1 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 22 | 92 | 2007 | 18.28 | 5.84 | 4.5 | 8.50000 | 28.0 | 300.0 | 550.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
| 23 | 50 | 2006 | 18.28 | 5.50 | 4.5 | 8.50000 | 28.0 | 330.0 | 600.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| 24 | 34 | 2015 | 18.28 | 5.80 | 4.5 | 8.00000 | 29.0 | 300.0 | 600.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 25 | 7 | 2007 | 18.28 | 5.90 | 4.5 | 8.13871 | 27.0 | 290.0 | 580.0 | 0 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| 26 | 59 | 2019 | 18.28 | 5.85 | 4.5 | 8.00000 | 29.0 | 350.0 | 560.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 27 | 1000 | 2008 | 18.28 | 5.85 | 4.5 | 8.50000 | 29.0 | 350.0 | 610.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 28 | 11 | 2019 | 18.28 | 5.85 | 4.5 | 8.00000 | 29.0 | 320.0 | 580.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 29 | 4 | 2019 | 18.28 | 5.70 | 4.5 | 8.00000 | 29.0 | 260.0 | 600.0 | 0 | ... | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 30 | 53 | 2007 | 18.28 | 5.50 | 4.5 | 7.70000 | 29.0 | 365.0 | 700.0 | 0 | ... | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| 31 | 99 | 2019 | 18.28 | 5.40 | 4.5 | 7.70000 | 29.0 | 330.0 | 630.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 32 | 2 | 2020 | 18.28 | 5.50 | 4.5 | 7.80000 | 28.0 | 270.0 | 535.0 | 0 | ... | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 |
| 33 | 6 | 2020 | 18.28 | 5.70 | 4.5 | 7.90000 | 27.3 | 270.0 | 535.0 | 0 | ... | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
33 rows × 35 columns
Maintenant que nous avons netroyé las données des deux cotés, nous pouvons faire une jointure en utilisant d'un coté l'attribut "Nat. / Sail" et de l'autre "Numéro de voile".
df_join = df_classement_clean.merge(df_all_boats_data_clean, left_on="Nat. / Sail", right_on = "Numéro de voile", how='left' )
print(df_join.dtypes)
print("total na",df_join.isna().sum().sum())
Rank int64 Nat. / Sail int64 Skipper / crew object Latitude float64 Longitude float64 30 minutes Heading float64 30 minutes Speed float64 30 minutes VMG float64 30 minutes Distance float64 last report Heading float64 last report Speed float64 last report VMG float64 last report Distance float64 24 hours Heading float64 24 hours Speed float64 24 hours VMG float64 24 hours Distance float64 DTF float64 DTL float64 Horodatage datetime64[ns] Numéro de voile int64 Date de lancement int64 Longueur float64 Largeur float64 Tirant d'eau float64 Déplacement (poids) float64 Hauteur mât float64 Surface de voiles au près float64 Surface de voiles au portant float64 Architecte_Bruce Farr Design uint8 Architecte_Groupe Finot-Conq uint8 Architecte_Juan Kouyoumdjian uint8 Architecte_Lavanos uint8 Architecte_Marc Lombard uint8 Architecte_Owen Clarke Design uint8 Architecte_Pierre Rolland uint8 Architecte_Samuel Manuard uint8 Architecte_VPLP uint8 Architecte_VPLP - Alex Thomson Racing (led by Pete Hobson) uint8 Architecte_Verdier uint8 Architecte_Verdier - VPLP uint8 Nombre de dérives_2 uint8 Nombre de dérives_2 asymétriques uint8 Nombre de dérives_foiler uint8 Nombre de dérives_foils uint8 Voile quille_ uint8 Voile quille_Acier mécano soudé uint8 Voile quille_Inox usiné uint8 Voile quille_acier uint8 Voile quille_acier forgé uint8 Voile quille_acier mécano soudé uint8 Voile quille_basculante avec vérin uint8 Voile quille_basculante sur vérin hydraulique uint8 Voile quille_carbone uint8 Voile quille_monotype uint8 dtype: object total na 0
Maintenant que nous avons entierement nettoyé notre dataFrame, nous pouvons passer à la partie la plus importante du projet : L'analyse afin d'en sortir des connaisance utiles
Commencons notre analyse par un peu de data viz afin de regarder les parcours des participants. Avec la carte ci dessous, nous pouvons voir l'ensemble des tracés de chaque compétiteur. Ainsi, il est possible de selectionner dans la légende le compétiteur qu'on sohaite afficher ou non.
Grâce à cet outils nous pouvons plus facilement nous rendre compte que Alex Tompson à arrété la compétion au niveau du cap de bon espérence, tansique Sébastien Destremau à prolongé sa route jusqu'à la nouvelle zelande.
fig = px.line_geo(df_join, lat="Latitude", lon="Longitude",
color="Skipper / crew", # "continent" is one of the columns of gapminder
hover_name="Skipper / crew",
hover_data=['Horodatage'],
projection="orthographic"
)
fig.show()
Maintenant passons à une analyse un peu plus sérieuse. Nous allons essayer dans cette partie de prédire la distance parcourue par les skippers en une journée et voir les paramètres qui influencent le plus la prédiction.
Pour cela nous avons séparé notre dataset en deux parties : entrainement et test, comme nous pouvons le voir çi-dessous.
Notons toutefois que nous conservons uniquement dans les variables explicatives x les valeurs qui ne sont pas liées aux valeurs des 24 dernières heures ainsi que celle permettant l'identification du bateau.
from sklearn.model_selection import train_test_split
Y = df_join["24 hours Distance"]
#on supprime toutes les données liée aux 24 h ainsi que celles permettant
X = df_join.drop(["24 hours Distance", "24 hours VMG", "24 hours Speed", "Skipper / crew", "Nat. / Sail", "Numéro de voile"], axis=1)
X["Horodatage"]= X["Horodatage"].apply(lambda x: x.value)
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=2632)
Puis nous normalisons nos datasets
from sklearn.preprocessing import StandardScaler
sc = StandardScaler()
sc.fit(X_train)
X_train_scaled = sc.transform(X_train)
X_test_scaled = sc.transform(X_test)
Pour réaliser notre régression linéaire nous utilisons un algorithme de régularisation de Lasso qui possède l'avantage d'attribuer à certaines variables peu intéressantes un coefficient nul dans la régression. Pour ne pas influencer le modèle en attribuant un paramètre $alpha$ arbitraire, nous choisissons de la faire varier et de prendre le meilleur, c'est-à-dire celui qui minimise l'erreur.
On regarde maintenant la performance de nos modèles via le coefficient de détermination $R^2$, qui est le ratio entre variance 'expliquée' et variance 'totale'. Plus il est proche de 1, meilleur est le modèle, car il est capable de déterminer une majorité de points.
from sklearn.linear_model import LassoCV
clf = LassoCV(alphas=np.arange(0.01, 1, 0.01))
clf.fit(X_train_scaled, y_train)
print("Meilleur alpha", clf.alpha_)
print("R2 DepDelay", clf.score(X_train_scaled, y_train))
Meilleur alpha 0.03 R2 DepDelay 0.5938767570288019
Comme nous pouvons le voir ci-dessus, nous obtenons un $\alpha$ de 0.03 qui est bien compris entre les bornes de notre intervalle de test. Cela signifie que nous avons trouvé un alpha optimal pour ce problème. Cependant, le $R^2$ est vraiment mauvais. Pour rappel: on considère que le $R^2$ commence à être acceptable à partir de 0.8.
Jetons un coup d'oeil aux paramètres utilisés par notre modèle
coeff_parameter = pd.DataFrame(data={"Variable":X.columns,"Coefficient":clf.coef_})
coeff_parameter = coeff_parameter[coeff_parameter.Coefficient>0]
coeff_parameter.sort_values(by=['Coefficient'])
| Variable | Coefficient | |
|---|---|---|
| 15 | Date de lancement | 0.006712 |
| 30 | Architecte_Samuel Manuard | 0.080877 |
| 40 | Voile quille_Acier mécano soudé | 0.213926 |
| 41 | Voile quille_Inox usiné | 0.251589 |
| 11 | 24 hours Heading | 0.470322 |
| 13 | DTL | 0.491646 |
| 45 | Voile quille_basculante avec vérin | 0.636608 |
| 24 | Architecte_Groupe Finot-Conq | 0.676347 |
| 22 | Surface de voiles au portant | 0.699016 |
| 21 | Surface de voiles au près | 0.781244 |
| 29 | Architecte_Pierre Rolland | 0.838773 |
| 6 | 30 minutes Distance | 0.921136 |
| 37 | Nombre de dérives_foiler | 1.024726 |
| 43 | Voile quille_acier forgé | 1.583483 |
| 31 | Architecte_VPLP | 1.899378 |
| 38 | Nombre de dérives_foils | 2.300818 |
| 2 | Longitude | 2.784066 |
| 12 | DTF | 7.810679 |
| 14 | Horodatage | 9.682857 |
| 9 | last report VMG | 13.764134 |
| 8 | last report Speed | 47.771982 |
Comme nous pouvons le constater dans le tableau récapitulatif ci-dessus, les paramètres les plus importants pour la prédiction des retards sont les données du dernier rapport (vitesse, VMG).
Bien entendu les principales caractérisitiques techniques du bateau influent grandement la distance, notamment en première position la présence d'un foil à la place des dérives.
La longitude et l'horodatage jouent également un rôle dans la distance parcourue, cela est sans doute lié à la présence de certains courants importants ou du vent (nous essayerons de voir cela dans la partie suivante).
Il est intéressant également de noter que certaines Architecte sont plus susceptibles de produire des bateaux parcourant plus de distance en 24H que d'autres (moralité : bien choisir la compagnie avec laquelle un skipper conçoit son bateau).
Refaisons la même analyse en incluant cette fois le skipper afin de voir si les résultats de la course sont uniquement dictés par la performance des bateaux ou bien également par la compétence du navigateur.
Y = df_join["24 hours Distance"]
#on supprime toutes les données liée aux 24 h ainsi que celles permettant
X = df_join.drop(["24 hours Distance", "24 hours VMG", "24 hours Speed", "Nat. / Sail", "Numéro de voile"], axis=1)
#transformation de la date en int
X["Horodatage"]= X["Horodatage"].apply(lambda x: x.value)
#Comme le sckipper est une varible catégorielle non ordonnée, nous devons la transformer en 1hot
X = pd.get_dummies(X)
X_train, X_test, y_train, y_test = train_test_split(X, Y, test_size=0.2, random_state=2632)
sc = StandardScaler()
sc.fit(X_train)
X_train_scaled = sc.transform(X_train)
X_test_scaled = sc.transform(X_test)
clf = LassoCV(alphas=np.arange(0.01, 1, 0.01))
clf.fit(X_train_scaled, y_train)
print("Meilleur alpha", clf.alpha_)
print("R2 DepDelay", clf.score(X_train_scaled, y_train))
coeff_parameter = pd.DataFrame(data={"Variable":X.columns,"Coefficient":clf.coef_})
coeff_parameter = coeff_parameter[coeff_parameter.Coefficient>0]
coeff_parameter.sort_values(by=['Coefficient'])
Meilleur alpha 0.01 R2 DepDelay 0.5949055659879552
| Variable | Coefficient | |
|---|---|---|
| 69 | Skipper / crew_Louis Burton | 0.006258 |
| 3 | 30 minutes Heading | 0.009282 |
| 74 | Skipper / crew_Pip Hare | 0.032937 |
| 33 | Architecte_Verdier | 0.041443 |
| 41 | Voile quille_Inox usiné | 0.043522 |
| 29 | Architecte_Pierre Rolland | 0.071878 |
| 71 | Skipper / crew_Maxime Sorel | 0.099645 |
| 65 | Skipper / crew_Jean Le Cam | 0.119965 |
| 64 | Skipper / crew_Isabelle Joschke | 0.150714 |
| 43 | Voile quille_acier forgé | 0.181291 |
| 53 | Skipper / crew_Armel Tripon | 0.219135 |
| 21 | Surface de voiles au près | 0.335726 |
| 80 | Skipper / crew_Thomas Ruyant | 0.367265 |
| 55 | Skipper / crew_Benjamin Dutreux | 0.505691 |
| 61 | Skipper / crew_Didac Costa | 0.537790 |
| 34 | Architecte_Verdier - VPLP | 0.561240 |
| 11 | 24 hours Heading | 0.569427 |
| 13 | DTL | 0.724886 |
| 76 | Skipper / crew_Samantha Davies | 0.878047 |
| 60 | Skipper / crew_Damien Seguin | 0.936272 |
| 30 | Architecte_Samuel Manuard | 0.999619 |
| 63 | Skipper / crew_Giancarlo Pedote | 1.030121 |
| 40 | Voile quille_Acier mécano soudé | 1.047330 |
| 37 | Nombre de dérives_foiler | 1.050305 |
| 66 | Skipper / crew_Jérémie Beyou | 1.403730 |
| 6 | 30 minutes Distance | 1.556055 |
| 2 | Longitude | 2.784211 |
| 9 | last report VMG | 14.207345 |
| 12 | DTF | 15.410078 |
| 14 | Horodatage | 17.846140 |
| 8 | last report Speed | 47.420108 |
Pour cette analyse nous conservons un $R^2$ semblable à la première, cependant nous voyons apparaître dans le tableau des coefficients que certains marins influencent le modèle comme : Jérémie Beyou, Giancarlo Pedote ou Samuel Manuard pour ne citer qu'eux. Nous avons donc bien montré que la compétence du skipper est également un facteur à prendre en compte dans la course.
Tel que mentionné en introduction, cette course est avant tout un voyage climatique pour descendre l'Atlantique, traverser l'océan Indien et le Pacifique, puis remonter de nouveau l'Atlantique... Les solitaires du Vendée Globe doivent en permanence jouer avec les systèmes météo. Ils sont composés d'anticyclones, zones de hautes pression plutôt stables et peu ventées et de dépressions, le plus souvent génératrices de vents forts.
Malheureusement, dans notre jeu de données, nous n'avons aucune information météorologique pourtant essentielle à la course.
Ainsi dans cette partie nous allons essayer de récupérer des données sur le vent, les courants marins ou encore les vagues à la position des compétiteurs. N'ayant pas trouvé une base de donnée textuelle, nous allons devoir la scrapper depuis le net. Pour cela nous utiliserons https://classic.nullschool.net/. Cependant, le gros problème de cette base est que la donnée se charge par javascript, elle n'est donc pas directement récupérable par la BeautifullSoup. Pour palier cette difficulté nous avons utilisé la libraire Selenium qui émule un navigateur Firefox
Le seul problème de cet algorithme est qu'il prend énormement de temps à s'éxécuter, en effet, il faut laisser à Selenium environ une seconde pour charger une page web. Comme nous avons un peu plus de 15000 positions dans notre dataframe, que nous devons pour chacune d'entre elles faire 3 appels au site et attendre à chaque fois 1 seconde, cela revient à attendre:
$15000 \times 3 = 45000 s = 12h30min$ c'est infaisable.
Toutefois, je vous mets le code totalement fonctionnel ci-dessous.
def get_wind_speed_direction(browser, row):
day = str(row["Horodatage"].day).zfill(2)
month = str(row["Horodatage"].month).zfill(2)
year = str(row["Horodatage"].year).zfill(4)
hour = str(row["Horodatage"].hour).zfill(2) + str(row["Horodatage"].minute).zfill(2)
url = "https://classic.nullschool.net/fr/#{}/{}/{}/{}Z/wind/surface/level/equirectangular/loc={},{}".format(year,month,day,hour,row["Longitude"],row["Latitude"])
browser.get(url)
sleep(1) #important le temps que que la page se charge
element_present = EC.presence_of_element_located((By.ID, 'location-wind'))
WebDriverWait(browser, 3).until(element_present)
page_source = browser.page_source
soup = BeautifulSoup(page_source, 'lxml')
wind = soup.find("span",id="location-wind").text
if wind:
wind_direction, wind_speed = re.findall("(\d+)° @ (\d+)", wind)[0]
row["wind_direction"] = int(wind_direction)
row["wind_speed"] = int(wind_speed)
return row
def get_wave_speed_direction(browser, row):
day = str(row["Horodatage"].day).zfill(2)
month = str(row["Horodatage"].month).zfill(2)
year = str(row["Horodatage"].year).zfill(4)
hour = str(row["Horodatage"].hour).zfill(2) + str(row["Horodatage"].minute).zfill(2)
url = "https://classic.nullschool.net/fr/#{}/{}/{}/{}Z/ocean/primary/waves/overlay=significant_wave_height/equirectangular/loc={},{}".format(year,month,day,hour,row["Longitude"],row["Latitude"])
browser.get(url)
sleep(1) #important le temps que que la page se charge
element_present = EC.presence_of_element_located((By.ID, 'location-wind'))
WebDriverWait(browser, 3).until(element_present)
page_source = browser.page_source
soup = BeautifulSoup(page_source, 'lxml')
wave = soup.find("span",id="location-wind").text
if wave :
wave_direction, wave_speed = re.findall("(\d+)° @ (\d+)", wave)[0]
row["wave_direction"] = int(wave_direction)
row["wave_speed"] = int(wave_speed)
else :
row["wave_direction"] = np.nan
row["wave_speed"] = np.nan
wave_height = soup.find("span",id="location-value").text
if wave_height:
row["wave_height"] = float(wave_height)
else:
row["wave_height"] = np.nan
return row
def get_ocean_current(browser, row):
day = str(row["Horodatage"].day).zfill(2)
month = str(row["Horodatage"].month).zfill(2)
year = str(row["Horodatage"].year).zfill(4)
hour = str(row["Horodatage"].hour).zfill(2) + str(row["Horodatage"].minute).zfill(2)
url = "https://classic.nullschool.net/fr/#{}/{}/{}/{}Z/ocean/surface/currents/equirectangular/loc={},{}".format(year,month,day,hour,row["Longitude"],row["Latitude"])
browser.get(url)
sleep(1) #important le temps que que la page se charge
element_present = EC.presence_of_element_located((By.ID, 'location-wind'))
WebDriverWait(browser, 3).until(element_present)
page_source = browser.page_source
soup = BeautifulSoup(page_source, 'lxml')
current = soup.find("span",id="location-wind").text
print(current)
if current:
current_direction, current_speed = re.findall("(\d+)° @ ([-+]?(?:\d*\.\d+|\d+))", current)[0]
row["current_direction"] = int(current_direction)
row["current_speed"] = float(current_speed)
print(row["current_speed"])
else:
row["current_direction"] = np.nan
row["current_speed"] = np.nan
return row
browser = webdriver.Firefox()
df_classement_enriched = pd.DataFrame()
for i, row in df_join.iterrows():
if i > 5000:
row = get_wind_speed_direction(browser, row)
row = get_wave_speed_direction(browser, row)
row = get_ocean_current(browser, row)
#je ne comprends pas pourquoi je n'arrive pas à ajouter en ligne dans ce cas on va prendre par la suite la transposée du dataframe
df_classement_enriched = pd.concat([df_classement_enriched, row], axis=1, ignore_index=True)
browser.close()
df_classement_enriched = df_classement_enriched.T
df_classement_enriched

Ainsi si nous avions pu récupérer l'information comme souhaité l'objectif suivant aurait été de prédire comme précédemment la distance parcourue avec ces nouveaux paramètre et voir si l'on obtient de meilleurs résultats et constater à quel point la météo possède un coefficient élevé dans la régression Lasso.
Dans ce projet nous nous sommes intéressé aux données du Vendée Globe 2020-2021. Pour cela, nous avons (non sans mal) récupéré des fichiers excels des classements provisoires de la course ainsi que les données techniques des bateaux. Par la suite nous les avons nettoyés/traités afin de rendre la donnée utilisable par un système d'analyse statistique.
Après avoir fait une petite carte pour montrer les parcours des participants, nous avons voulu savoir quels était les paramètres qui influancent le plus la distance parcourue par les marins en 24 heures pour cela nous avons utilisé une régression Lasso. Nous avons bien entendu observé que les principales caractéristiques techniques du bateau influent grandement la distance, notamment en première position la présence d'un foil à la place des dérives. Nous avons également constaté que les concepteurs/architectes des bateaux ont eux aussi une influence dans le résultat.
En voyant cela nous avons voulu savoir si la course est uniquement dictée par le matériel ou si la compétence du skipper agit,elles aussi, sur le résultat. Ainsi dans un second temps nous avons bien observé que le marin influence positivement les variables du modèle sans toutefois améliorer la qualité de la prédiction.
Ainsi dans un dernier temps nous avons voulu récupérer le facteur le plus important de la course : la météo et plus particulièrement la force et la direction du vent, le sens des courants marins et la direction et hauteur des vagues. Malheureusement, face aux temps de calcul extrêmement long, nous n'avons pas pu finir cette analyse.
C'était un très bon projet, très intéressant qui ferait un bon sujet de hackaton. Je ne doute pas de ma capacité à mener à terme toutes les analyses souhaitées si j'avais eu un peu plus de temps et la possibilité de répartir le scrapping sur plusieurs ordinateurs.